{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Basics of 1d calibrators\n",
    "In this notebook, we'll explain one dimensional calibrators."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First we need to import libraries we're going to use."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import tensorflow as tf\n",
    "import tensorflow_lattice as tfl\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import math"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Next, let's prepare a synthetic dataset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Example function we will try to learn with calibration.\n",
    "%matplotlib inline\n",
    "def f(x):\n",
    "    return np.power(x, 3) + 0.1*np.sin(x * math.pi * 8)\n",
    "\n",
    "def gen_examples(n):\n",
    "    x = np.random.uniform(-1, 1.0, size=n)\n",
    "    x.sort()\n",
    "    y = f(x)\n",
    "    return (x,y)\n",
    "\n",
    "# pwl_x_data and pwl_y_data contains a synthetic dataset.\n",
    "n = 100\n",
    "pwl_x_data, pwl_y_data = gen_examples(n)\n",
    "\n",
    "plt.plot(pwl_x_data, pwl_y_data)\n",
    "plt.ylabel(\"y\")\n",
    "plt.xlabel(\"x\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Fitting a Piecewise Linear calibartor\n",
    "In a piecewise linear (PWL) calibrator, input keypoints are given and output keypoints are TensorFlow variables.\n",
    "So we fitting the output keypoints to minimize a loss function."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "# Let's reset the default graph to clean up the state.\n",
    "tf.reset_default_graph()\n",
    "\n",
    "# x is a placeholder for feeding 1d input data.\n",
    "# We will feed the full datapoints (100), i.e., batch_size == 100.\n",
    "x = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "# y is a placeholder for feeding ground truth output.\n",
    "y_ = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "\n",
    "\n",
    "# To use calibrator, we need to initialize input and output keypoints.\n",
    "# Here we'll use 50 number of keypoints in the PWL calibrator.\n",
    "# Here 50 input keypoints will be uniformly spaced over [-1, 1], and 50 output\n",
    "# keypoints will be uniformly spaced over [-0.5, 0.5].\n",
    "#\n",
    "# During training input keypoints will not be changed, but output keypoints will\n",
    "# be changed to fit our data.\n",
    "#\n",
    "# Calibrator will clip the input outside of the input range [-1, 1], which means\n",
    "# the input value less than -1 will be clipped to -1, and the input value\n",
    "# greater than +1 will be clipped to +1. Feel free to change input_min and\n",
    "# input_max to see this behavior.\n",
    "num_keypoints = 50\n",
    "kp_inits = tfl.uniform_keypoints_for_signal(\n",
    "    num_keypoints=num_keypoints,\n",
    "    input_min=-1.0,\n",
    "    input_max=1.0,\n",
    "    output_min=-0.5,\n",
    "    output_max=0.5)\n",
    "\n",
    "# Now we define PWL linear calibrator with 50 keypoints that calibrate the input\n",
    "# tensor x (with shape [batch_size]), to the output tensor y (with shape\n",
    "# [batch_size]). y[0] is the calibrated x[0], y[1] is the calibrated x[1], ....\n",
    "# calibration_layer returns three elements:\n",
    "#  1. output tensor\n",
    "#  2. Projection operator\n",
    "#  3. Regularization loss (scalar tensor)\n",
    "# We'll cover the second and the third part in this notebook as well, so let's\n",
    "# focus on the first.\n",
    "(y, _, _) = tfl.calibration_layer(\n",
    "    uncalibrated_tensor=x,\n",
    "    num_keypoints=num_keypoints,\n",
    "    keypoints_initializers=kp_inits)\n",
    "\n",
    "# To train a calibrator, we define L2 loss.\n",
    "# Here y_ is the ground truth.\n",
    "loss = tf.reduce_mean(tf.square(y - y_))\n",
    "\n",
    "# Now we define TensorFlow training operator.\n",
    "# Here we'll use GradientDescentOptimizer with the initial learning rate 0.1.\n",
    "# This train_op computes the gradient of L2 \"loss\" we just defined w.r.t. the \n",
    "# output keypoints in the calibrator, and update output keypoints value to\n",
    "# minimize L2 \"loss\".\n",
    "train_op = tf.train.GradientDescentOptimizer(learning_rate=0.1).minimize(loss)\n",
    "\n",
    "sess = tf.Session()\n",
    "# Before starting to train, we need to initialize variables in our computational\n",
    "# graph.\n",
    "sess.run(tf.global_variables_initializer())\n",
    "\n",
    "# Apply gradient descent operator 1000 times.\n",
    "for _ in range(1000):\n",
    "    # Update output keypoints by feeding the full data into our computational\n",
    "    # graph.\n",
    "    sess.run(train_op, feed_dict={x: pwl_x_data, y_: pwl_y_data})\n",
    "\n",
    "# Now training is done, let us fetch the prediction from our calibrator.\n",
    "# predicted will contain numpy n-d array of predictions over pwl_x_data.\n",
    "predicted = sess.run(y, feed_dict={x: pwl_x_data})\n",
    "\n",
    "# Plot the response.\n",
    "plt.plot(pwl_x_data, predicted)\n",
    "plt.plot(pwl_x_data, pwl_y_data)\n",
    "plt.ylabel(\"predicted\")\n",
    "plt.xlabel(\"x\")\n",
    "plt.legend(['PWL calibrator', 'True Data'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Bounded PWL calibrator\n",
    "By default, output keypoints in the calibrator are not bounded.\n",
    "In some cases, this is not desirable, especially when the output of the calibrator is fed into upper layer.\n",
    "For example, a 2 x 2 lattice expects an input is in [0, 1] x [0, 1], so if calibrator output is fed into such a lattice layer,\n",
    "it would be better for output keypoints to be in the range [0, 1]."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "# Same as before.\n",
    "tf.reset_default_graph()\n",
    "\n",
    "x = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "y_ = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "\n",
    "kp_inits = tfl.uniform_keypoints_for_signal(\n",
    "    num_keypoints=50,\n",
    "    input_min=-1.0,\n",
    "    input_max=1.0,\n",
    "    output_min=-0.5,\n",
    "    output_max=0.5)\n",
    "\n",
    "# Now we define a calibrator with \"bound\".\n",
    "# By setting bound == True, we are making sure the output keypoints are inside\n",
    "# the initial output range from kp_inits, [-0.5, 0.5].\n",
    "# This is acheived by projection_op. projection_op is a collection of TensorFlow\n",
    "# operators that find the output keypoints not in the range [-0.5, 0.5] and\n",
    "# assign 0.5 for output keypoints larger than 0.5, -0.5 for output keypoints\n",
    "# smaller than -0.5.\n",
    "(y, projection_op, _) = tfl.calibration_layer(\n",
    "    uncalibrated_tensor=x,\n",
    "    num_keypoints=50,\n",
    "    bound=True,\n",
    "    keypoints_initializers=kp_inits)\n",
    "\n",
    "# Sqaured loss\n",
    "loss = tf.reduce_mean(tf.square(y - y_))\n",
    "\n",
    "# Minimize!\n",
    "train_op = tf.train.GradientDescentOptimizer(learning_rate=0.1).minimize(loss)\n",
    "\n",
    "sess = tf.Session()\n",
    "sess.run(tf.global_variables_initializer())\n",
    "# Iterate 1000 times\n",
    "for _ in range(1000):\n",
    "    # Apply gradient.\n",
    "    sess.run(train_op, feed_dict={x: pwl_x_data, y_: pwl_y_data})\n",
    "    # Then apply projection. This is projected SGD.\n",
    "    sess.run(projection_op)\n",
    "\n",
    "predicted = sess.run(y, feed_dict={x: pwl_x_data})\n",
    "\n",
    "# In the plot, we should see that the predictions are in the range [-0.5, 0.5].\n",
    "plt.plot(pwl_x_data, predicted)\n",
    "plt.plot(pwl_x_data, pwl_y_data)\n",
    "plt.ylabel(\"predicted\")\n",
    "plt.xlabel(\"x\")\n",
    "plt.legend(['PWL calibrator', 'True Data'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Bounded monotonic PWL calibrator\n",
    "You can also set monotonicity in each calibrator."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "tf.reset_default_graph()\n",
    "\n",
    "x = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "y_ = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "\n",
    "kp_inits = tfl.uniform_keypoints_for_signal(\n",
    "    num_keypoints=50,\n",
    "    input_min=-1.0,\n",
    "    input_max=1.0,\n",
    "    output_min=-0.5,\n",
    "    output_max=0.5)\n",
    "\n",
    "# Monotonically increasing 1d calibrator.\n",
    "# In addition to the bound, now let's make the calibrator monotonic.\n",
    "# Since we set monotonic to +1, projection_op now contains not only the bounding\n",
    "# projection, but also monotonicity projection.\n",
    "(y, projection_op, _) = tfl.calibration_layer(\n",
    "    uncalibrated_tensor=x,\n",
    "    num_keypoints=50,\n",
    "    bound=True,\n",
    "    monotonic=+1,\n",
    "    keypoints_initializers=kp_inits)\n",
    "\n",
    "# Sqaured loss\n",
    "loss = tf.reduce_mean(tf.square(y - y_))\n",
    "\n",
    "# Minimize!\n",
    "train_op = tf.train.GradientDescentOptimizer(learning_rate=0.1).minimize(loss)\n",
    "\n",
    "sess = tf.InteractiveSession()\n",
    "sess.run(tf.global_variables_initializer())\n",
    "# Iterate 1000 times\n",
    "for _ in range(1000):\n",
    "    # Apply gradient.\n",
    "    sess.run(train_op, feed_dict={x: pwl_x_data, y_: pwl_y_data})\n",
    "    # Apply projection.\n",
    "    sess.run(projection_op)\n",
    "\n",
    "predicted = sess.run(y, feed_dict={x: pwl_x_data})\n",
    "\n",
    "# Now in the plot, we should see monotonically increasing predictions.\n",
    "plt.plot(pwl_x_data, predicted)\n",
    "plt.plot(pwl_x_data, pwl_y_data)\n",
    "plt.ylabel(\"predicted\")\n",
    "plt.xlabel(\"x\")\n",
    "plt.legend(['PWL calibrator', 'True Data'])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Laplacian regularizer\n",
    "Now let's add the Laplacian regularizer.\n",
    "Laplacian regularizer penalizes the change in consecutive output keypoint values.\n",
    "Therefore we can get much smoother 1d calibration result."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "\n",
    "tf.reset_default_graph()\n",
    "\n",
    "x = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "y_ = tf.placeholder(dtype=tf.float32, shape=(n))\n",
    "\n",
    "kp_inits = tfl.uniform_keypoints_for_signal(\n",
    "    num_keypoints=50,\n",
    "    input_min=-1.0,\n",
    "    input_max=1.0,\n",
    "    output_min=-0.5,\n",
    "    output_max=0.5)\n",
    "\n",
    "# Piecewise linear calibration.\n",
    "# Here we set L2 Laplacian regularization.\n",
    "# L2 Laplacian regularization ==\n",
    "#  ||output_keypoints[1:end] - output_keypoints[0:-2]||_2^2\n",
    "# which penalizes changes in consecutive output keypoints (the slope) in the\n",
    "# calibrator.\n",
    "# regularization is 1d scalar, and we expect this to be added to the loss.\n",
    "(y, projection_op, regularization) = tfl.calibration_layer(\n",
    "    uncalibrated_tensor=x,\n",
    "    num_keypoints=50,\n",
    "    bound=True,\n",
    "    monotonic=+1,\n",
    "    l2_laplacian_reg=0.1,\n",
    "    keypoints_initializers=kp_inits)\n",
    "\n",
    "# loss == squared loss + regularzation.\n",
    "loss = tf.reduce_mean(tf.square(y - y_))\n",
    "loss += regularization\n",
    "\n",
    "# Minimize!\n",
    "train_op = tf.train.GradientDescentOptimizer(learning_rate=0.1).minimize(loss)\n",
    "\n",
    "sess = tf.InteractiveSession()\n",
    "sess.run(tf.global_variables_initializer())\n",
    "# Iterate 10 times\n",
    "for _ in range(1000):\n",
    "    # Apply gradient.\n",
    "    sess.run(train_op, feed_dict={x: pwl_x_data, y_: pwl_y_data})\n",
    "    sess.run(projection_op)\n",
    "\n",
    "predicted = sess.run(y, feed_dict={x: pwl_x_data})\n",
    "# Now we expect a smoother calibrator.\n",
    "plt.plot(pwl_x_data, predicted)\n",
    "plt.plot(pwl_x_data, pwl_y_data)\n",
    "plt.ylabel(\"predicted\")\n",
    "plt.xlabel(\"x\")\n",
    "plt.legend(['PWL calibrator', 'True Data'])"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "tf-lattice",
   "language": "python",
   "name": "tf-lattice"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.10"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
